{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Conversational Workflows with MCP: A Marie Antoinette Take on The Eiffel Tower\n",
    "\n",
    "**Authors:** Licong Xu and Boris Bolliet (Cambridge)\n",
    "\n",
    "**Original Code:** [MCPAgents](https://github.com/CMBAgents/MCPAgents)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "1",
   "metadata": {},
   "source": [
    "## Imports and Setup\n",
    "\n",
    "This cell sets up the environment and imports necessary libraries:\n",
    "\n",
    "- **Pathlib**, **os**, **json**, **asyncio**, etc. for file and system operations.\n",
    "- Imports from the `autogen` and `mcp` libraries, which are used to create conversational agents and connect to a filesystem-related tool server.\n",
    "- `nest_asyncio.apply()` ensures that asynchronous code runs properly in Jupyter notebooks.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "\n",
    "# Only needed for Jupyter notebooks\n",
    "import nest_asyncio\n",
    "from mcp import ClientSession, StdioServerParameters\n",
    "from mcp.client.stdio import stdio_client\n",
    "\n",
    "from autogen import LLMConfig\n",
    "from autogen.mcp import create_toolkit\n",
    "\n",
    "nest_asyncio.apply()\n",
    "\n",
    "import copy\n",
    "import os\n",
    "from datetime import timedelta\n",
    "\n",
    "from pydantic import BaseModel, Field\n",
    "\n",
    "from autogen import ConversableAgent, UpdateSystemMessage\n",
    "from autogen.agentchat import a_initiate_group_chat\n",
    "from autogen.agentchat.group import (\n",
    "    AgentTarget,\n",
    "    ContextVariables,\n",
    "    OnCondition,\n",
    "    StringLLMCondition,\n",
    "    TerminateTarget,\n",
    ")\n",
    "from autogen.agentchat.group.patterns import (\n",
    "    DefaultPattern,\n",
    ")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "3",
   "metadata": {},
   "source": [
    "## Define MCP Server Path\n",
    "\n",
    "Set the path to the **MCP server script**, which will be used to handle tool execution related to the filesystem retrieval.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4",
   "metadata": {},
   "outputs": [],
   "source": [
    "mcp_server_path = Path(\"mcp/mcp_filesystem.py\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "5",
   "metadata": {},
   "source": [
    "## Agents\n",
    "\n",
    "- Define the **joker agent**, whose role is to make jokes in the style of Marie Antoinette.\n",
    "- Use a `pydantic` model to structure the joke and explanation.\n",
    "- Set LLM configuration, including temperature, caching, and model.\n",
    "- Define a `ContextVariables` object to inject context (like joke constraints) into the agent's workflow.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6",
   "metadata": {},
   "outputs": [],
   "source": [
    "joker_message = \"\"\"\n",
    "You are the joker in the team. You make jokes.\n",
    "\n",
    "You must obey the following constraints:\n",
    "\n",
    "{joke_constraints}\n",
    "\n",
    "\"\"\".strip()\n",
    "\n",
    "\n",
    "class JokeResponse(BaseModel):\n",
    "    joke_instructions: str = Field(..., description=\"instruction, not in the style of Marie Antoinette\")\n",
    "    joke: str = Field(\n",
    "        ..., description=\"The joke. The joke must be in the style of Marie Antoinette and mention Louis the XIVth.\"\n",
    "    )\n",
    "    joke_explanation: str = Field(..., description=\"explanation, not in the style of Marie Antoinette\")\n",
    "\n",
    "    def format(self) -> str:\n",
    "        return \"\\n\".join([\n",
    "            \"**Joke instructions:**\",\n",
    "            \"\",\n",
    "            self.joke_instructions,\n",
    "            \"\",\n",
    "            \"**Joke:**\",\n",
    "            \"\",\n",
    "            self.joke,\n",
    "            \"\",\n",
    "            \"**Joke explanation:**\",\n",
    "            \"\",\n",
    "            self.joke_explanation,\n",
    "        ])\n",
    "\n",
    "\n",
    "default_llm_config = {\n",
    "    \"cache_seed\": 42,\n",
    "    \"temperature\": 1.0,\n",
    "    \"top_p\": 0.05,\n",
    "    \"config_list\": [{\"model\": \"gpt-4.1\", \"api_key\": os.getenv(\"OPENAI_API_KEY\"), \"api_type\": \"openai\"}],\n",
    "    \"timeout\": 1200,\n",
    "}\n",
    "\n",
    "joker_config_list = copy.deepcopy(default_llm_config)\n",
    "joker_config_list[\"config_list\"][0][\"response_format\"] = JokeResponse\n",
    "\n",
    "\n",
    "joker = ConversableAgent(\n",
    "    name=\"joker\",\n",
    "    system_message=joker_message,\n",
    "    llm_config=joker_config_list,\n",
    "    update_agent_state_before_reply=[\n",
    "        UpdateSystemMessage(joker_message),\n",
    "    ],\n",
    ")\n",
    "\n",
    "workflow_context = ContextVariables(\n",
    "    data={\n",
    "        \"joke_constraints\": \"the joke should make use of the contextual information passed on to you. It should be a paragraph long and use as much detailed information from the context as possible.\",\n",
    "    }\n",
    ")\n",
    "\n",
    "\n",
    "task = \"\"\"\n",
    "Read the file in context and Make a joke.\n",
    "\"\"\"\n",
    "\n",
    "initial_agent = joker"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "id": "7",
   "metadata": {},
   "source": [
    "## Create Toolkit and Run\n",
    "\n",
    "1. Asynchronously create a **toolkit** from the client session and registers it to a `mcp_agent` that will search and load local files.\n",
    "2. Set up a **handoff**: once `mcp_agent` finishes its task, it passes control to `joker`.\n",
    "3. Delete the `.cache/` folder to reset the environment.\n",
    "4. Initialize a **DefaultPattern** for how agents interact.\n",
    "5. Start the **group chat workflow** using `a_initiate_group_chat`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8",
   "metadata": {},
   "outputs": [],
   "source": [
    "async def create_toolkit_and_run(session: ClientSession) -> None:\n",
    "    # Create a toolkit with available MCP tools\n",
    "    toolkit = await create_toolkit(session=session)\n",
    "    mcp_agent = ConversableAgent(\n",
    "        name=\"mcp_agent\",\n",
    "        system_message=r\"\"\"\n",
    "Read the file in your folder.\n",
    "                             \"\"\",\n",
    "        llm_config=LLMConfig(config_list={\"model\": \"gpt-5\", \"tool_choice\": \"required\"}),\n",
    "    )\n",
    "    # Register MCP tools with the agent\n",
    "    toolkit.register_for_llm(mcp_agent)\n",
    "\n",
    "    toolkit.register_for_execution(mcp_agent)\n",
    "\n",
    "    # joker.handoffs.set_after_work(AgentTarget(mcp_agent))\n",
    "    joker.handoffs.set_after_work(TerminateTarget())\n",
    "\n",
    "    mcp_agent.handoffs.set_after_work(AgentTarget(joker))\n",
    "\n",
    "    mcp_agent.handoffs.add_llm_conditions([\n",
    "        OnCondition(\n",
    "            target=AgentTarget(joker),\n",
    "            condition=StringLLMCondition(prompt=\"The file has been read.\"),\n",
    "        ),\n",
    "    ])\n",
    "\n",
    "    agents = [\n",
    "        joker,\n",
    "        mcp_agent,\n",
    "    ]\n",
    "\n",
    "    for agent in agents:\n",
    "        agent.reset()\n",
    "    print(\"all agents reset\")\n",
    "\n",
    "    import os\n",
    "    import shutil\n",
    "\n",
    "    def delete_cache_folder():\n",
    "        cache_path = os.path.join(os.getcwd(), \".cache\")\n",
    "        if os.path.isdir(cache_path):\n",
    "            shutil.rmtree(cache_path)\n",
    "            print(\".cache folder deleted.\")\n",
    "        else:\n",
    "            print(\"No .cache folder found in current directory.\")\n",
    "\n",
    "    delete_cache_folder()\n",
    "\n",
    "    # Create the pattern\n",
    "    agent_pattern = DefaultPattern(\n",
    "        agents=[joker, mcp_agent],\n",
    "        initial_agent=mcp_agent,\n",
    "        context_variables=workflow_context,\n",
    "    )\n",
    "\n",
    "    await a_initiate_group_chat(\n",
    "        pattern=agent_pattern,\n",
    "        messages=task,\n",
    "        max_rounds=20,\n",
    "    )\n",
    "\n",
    "\n",
    "# Create server parameters for stdio connection\n",
    "server_params = StdioServerParameters(\n",
    "    command=\"python\",  # The command to run the server\n",
    "    args=[\n",
    "        str(mcp_server_path),\n",
    "        \"stdio\",\n",
    "        \"--context-path\",\n",
    "        \"mcp/context_docs\",\n",
    "    ],  # Path to server script and transport mode\n",
    ")\n",
    "\n",
    "async with (\n",
    "    stdio_client(server_params) as (read, write),\n",
    "    ClientSession(read, write, read_timeout_seconds=timedelta(seconds=30)) as session,\n",
    "):\n",
    "    # Initialize the connection\n",
    "    await session.initialize()\n",
    "    await create_toolkit_and_run(session)"
   ]
  }
 ],
 "metadata": {
  "front_matter": {
   "description": "Conversational Workflows with MCP: A Marie Antoinette Take on The Eiffel Tower",
   "tags": [
    "MCP"
   ]
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
